package jmine.tec.utils.collection.impl;

import java.io.Serializable;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.ObjectUtils;

/**
 * Encapsula uma coleção e armazena um {@link HashMap} como cache para seus elementos possibilitando a pesquisa de um valor através de uma
 * chave. <br>
 * <br>
 * A coleção encapsulada não deve ser modificada diretamente. Todas as operações deveram ser executados neste wrapper para que o <i>
 * {@link #getMap() map}</i> disponibilizado esteja sempre atualizado.<br>
 * <br>
 * Cada vez que uma alteração é efetuada será gravado uma <i>{@link #getModificationCount() flag}</i> indicando que o cache do <i>
 * {@link #getMap() map}</i> precisa ser reconstruído na próxima vez que for acessado.
 * 
 * @param <K> Tipo da chave
 * @param <V> Tipo do valor
 * @param <E> Tipo de elemento da coleção
 * @author wellington.santos
 * @created Feb 14, 2012
 */
public abstract class HashMapCollectionWrapper<K, V, E> implements Collection<E>, Serializable {

    protected transient int modificationCount;

    protected transient MapImpl map;

    /**
     * Indica quantas modificações foram efeituadas nesta coleção.
     * 
     * @return o total de modificações foram efeituadas nesta coleção
     */
    public final int getModificationCount() {
        return this.modificationCount;
    }

    /**
     * Incrementa modificações foram efeituadas nesta coleção
     * 
     * @return o total de modificações foram efeituadas nesta coleção
     */
    public int notifyModification() {
        return ++this.modificationCount;
    }

    /**
     * Obtém a coleção de elementos original.
     * 
     * @return a coleção de elementos original
     */
    protected abstract Collection<E> getSource();

    /**
     * Obtém a chave de um elemento.
     * 
     * @param element elemento
     * @return a chave
     */
    protected abstract K getKey(E element);

    /**
     * Obtém o valor que corresponde a chave do elemento.
     * 
     * @param element elemento
     * @return o valor que corresponde a chave do elemento
     */
    protected abstract V getValue(E element);

    /**
     * Obtém um {@link Map} com a cache dos elementos desta coleção indexados pela chave para obter o valor.
     * 
     * @return um {@link Map} com uma cache dos elementos desta coleção
     */
    public Map<K, V> getMap() {
        if (this.map == null) {
            this.map = new MapImpl();
        }
        return this.map;
    }

    //
    // Membros de Collection que causam modificações
    //

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private Iterator<E> it = HashMapCollectionWrapper.this.getSource().iterator();

            public boolean hasNext() {
                return this.it.hasNext();
            }

            public E next() {
                return this.it.next();
            }

            public void remove() {
                HashMapCollectionWrapper.this.notifyModification();
                this.it.remove();
            }
        };
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean add(E o) {
        boolean modified = this.getSource().add(o);
        if (modified) {
            this.notifyModification();
        }
        return modified;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean remove(Object o) {
        boolean modified = this.getSource().remove(o);
        if (modified) {
            this.notifyModification();
        }
        return modified;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = this.getSource().addAll(c);
        if (modified) {
            this.notifyModification();
        }
        return modified;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean removeAll(Collection<?> c) {
        boolean modified = this.getSource().removeAll(c);
        if (modified) {
            this.notifyModification();
        }
        return modified;
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean retainAll(Collection<?> c) {
        boolean modified = this.getSource().retainAll(c);
        if (modified) {
            this.notifyModification();
        }
        return modified;
    }

    /**
     * {@inheritDoc}
     */
    public void clear() {
        if (!this.isEmpty()) {
            this.getSource().clear();
            this.notifyModification();
        }
    }

    //
    // Membros somente-leitura de Collection
    //

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public int size() {
        return this.getSource().size();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean isEmpty() {
        return this.getSource().isEmpty();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean contains(Object o) {
        return this.getSource().contains(o);
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public Object[] toArray() {
        return this.getSource().toArray();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public <T> T[] toArray(T[] a) {
        return this.getSource().toArray(a);
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    public boolean containsAll(Collection<?> c) {
        return this.getSource().containsAll(c);
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    public boolean equals(Object o) {
        return this.getSource().equals(o);
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return this.getSource().hashCode();
    }

    /**
     * {@inheritDoc}
     * 
     * @return {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.getSource().toString();
    }

    /**
     * Implementação do {@link Map} exposto por esta coleção.
     * 
     * @author wellington.santos
     * @created Feb 14, 2012
     */
    protected class MapImpl extends AbstractMap<K, V> implements Serializable {

        protected final SetImpl<K> keySet;

        protected final CollectionImpl<V> values;

        protected final SetImpl<Entry<K, V>> entrySet;

        protected transient HashMap<K, V> cache;

        protected transient int lastModification;

        /**
         * Construtor padrão.
         */
        public MapImpl() {
            this.entrySet = new SetImpl<Entry<K, V>>() {
                @Override
                public boolean remove(Object entry) {
                    return (entry instanceof Entry<?, ?>) && MapImpl.this.keySet.remove(((Entry<?, ?>) entry).getKey());
                }

                @Override
                protected Set<Entry<K, V>> getCollection() {
                    return MapImpl.this.cache.entrySet();
                }
            };
            this.keySet = new SetImpl<K>() {
                @Override
                public boolean remove(Object key) {
                    if (this.getCollection().contains(key)) {
                        MapImpl.this.removeByKey(key);
                        return true;
                    }
                    return false;
                }

                @Override
                protected Set<K> getCollection() {
                    return MapImpl.this.cache.keySet();
                }
            };
            this.values = new CollectionImpl<V>() {
                @Override
                public boolean remove(Object value) {
                    return MapImpl.this.removeByValue(value);
                }

                @Override
                protected Collection<V> getCollection() {
                    return MapImpl.this.cache.values();
                }
            };
        }

        /**
         * Atualiza os elementos do {@link Map} de cache.
         */
        protected void updateCache() {
            if (this.lastModification == 0 || this.lastModification != HashMapCollectionWrapper.this.modificationCount + 1) {
                this.lastModification = HashMapCollectionWrapper.this.modificationCount + 1;
                if (this.cache == null) {
                    this.cache = new HashMap<K, V>(HashMapCollectionWrapper.this.getSource().size());
                } else {
                    this.cache.clear();
                }
                for (E element : HashMapCollectionWrapper.this.getSource()) {
                    this.cache.put(HashMapCollectionWrapper.this.getKey(element), HashMapCollectionWrapper.this.getValue(element));
                }
            }
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public int size() {
            this.updateCache();
            return this.cache.size();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean isEmpty() {
            return HashMapCollectionWrapper.this.isEmpty();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void clear() {
            HashMapCollectionWrapper.this.clear();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public V get(Object key) {
            this.updateCache();
            return this.cache.get(key);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean containsKey(Object key) {
            this.updateCache();
            return this.cache.containsKey(key);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public V put(K key, V value) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void putAll(Map<? extends K, ? extends V> m) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public V remove(Object key) {
            this.updateCache();
            if (this.cache.keySet().contains(key)) {
                return HashMapCollectionWrapper.this.getValue(this.removeByKey(key));
            }
            return null;
        }

        /**
         * Remove uma {@link java.util.Map.Entry} se encontrada pela chave especificada.
         * 
         * @param key chave da {@link java.util.Map.Entry}
         * @return o elemento se foi removido da coleção ou <code>null</code>
         */
        public E removeByKey(Object key) {
            E result = null;
            Iterator<E> it = HashMapCollectionWrapper.this.iterator();
            while (it.hasNext()) {
                E element = it.next();
                if (ObjectUtils.equals(key, HashMapCollectionWrapper.this.getKey(element))) {
                    it.remove();
                    result = element;
                    // não chamar "break"
                }
            }
            return result;
        }

        /**
         * Remove uma {@link java.util.Map.Entry} se encontrada pelo valor especificado.
         * 
         * @param value valor da {@link java.util.Map.Entry}
         * @return True se removido da coleção
         */
        public boolean removeByValue(Object value) {
            boolean removed = false;
            Iterator<E> it = HashMapCollectionWrapper.this.iterator();
            while (it.hasNext()) {
                E element = it.next();
                if (ObjectUtils.equals(value, HashMapCollectionWrapper.this.getValue(element))) {
                    it.remove();
                    removed = true;
                    // não chamar "break"
                }
            }
            return removed;
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean containsValue(Object value) {
            Iterator<E> it = HashMapCollectionWrapper.this.iterator();
            while (it.hasNext()) {
                E element = it.next();
                if (ObjectUtils.equals(value, HashMapCollectionWrapper.this.getValue(element))) {
                    return true;
                }
            }
            return false;
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Object clone() {
            this.updateCache();
            return this.cache.clone();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Set<K> keySet() {
            return this.keySet;
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Collection<V> values() {
            return this.values;
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Set<Entry<K, V>> entrySet() {
            return this.entrySet;
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean equals(Object o) {
            this.updateCache();
            return this.cache.equals(o);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public int hashCode() {
            this.updateCache();
            return this.cache.hashCode();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public String toString() {
            this.updateCache();
            return this.cache.toString();
        }

    }

    /**
     * Implementação das {@link Set Sets} expostas por esta coleção.
     * 
     * @param <T> Tipo do elemento
     * @author wellington.santos
     * @created Feb 14, 2012
     */
    protected abstract class SetImpl<T> extends CollectionImpl<T> implements Set<T> {

        /**
         * Obtém a coleção de origem.
         * 
         * @return a coleção de origem
         */
        @Override
        protected abstract Set<T> getCollection();

    }

    /**
     * Implementação genérica das {@link Collection Collections} expostas por esta coleção.
     * 
     * @param <T> Tipo do elemento
     * @author wellington.santos
     * @created Feb 14, 2012
     */
    protected abstract class CollectionImpl<T> extends AbstractCollection<T> implements Serializable {

        /**
         * Obtém a coleção de origem.
         * 
         * @return a coleção de origem
         */
        protected abstract Collection<T> getCollection();

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Iterator<T> iterator() {
            HashMapCollectionWrapper.this.map.updateCache();
            return new Iterator<T>() {
                private Iterator<T> it = CollectionImpl.this.getCollection().iterator();

                private Iterator<E> it2 = HashMapCollectionWrapper.this.iterator();

                public boolean hasNext() {
                    return this.it2.hasNext() && this.it.hasNext();
                }

                public T next() {
                    this.it2.next();
                    return this.it.next();
                }

                public void remove() {
                    this.it2.remove();
                    this.it.remove();
                }
            };
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void clear() {
            HashMapCollectionWrapper.this.clear();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public int size() {
            return HashMapCollectionWrapper.this.getMap().size();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean isEmpty() {
            return HashMapCollectionWrapper.this.isEmpty();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public Object[] toArray() {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().toArray();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public <R> R[] toArray(R[] a) {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().toArray(a);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean add(T o) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean contains(Object o) {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().contains(o);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean addAll(Collection<? extends T> c) {
            throw new UnsupportedOperationException();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean equals(Object o) {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().equals(o);
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public int hashCode() {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().hashCode();
        }

        /**
         * {@inheritDoc}
         * 
         * @return {@inheritDoc}
         */
        @Override
        public boolean containsAll(Collection<?> c) {
            HashMapCollectionWrapper.this.map.updateCache();
            return this.getCollection().containsAll(c);
        }

    }

}
